今天會介紹常用來處理非同步程式的語法:Promise。在ES6的Promise語法出現之前,我們都依賴回傳函式來寫非同步程式,但Promise的出現,令整個非同步程式的流程結構更易讀更易維護。
本文會解釋:
Promise.all、Promise.race)XMLhttprequest
Promise與回傳函式一樣,同樣是用來處理非同步程式。最常用到非同步程式的情況,都是跟網絡連線有關(例如fetch抓取遠端資料),或者跟排程有關(例如setTimeout)。
不同的是,Promise改善了非同步的語法結構,比起只用回傳函式,更易閱讀和維護。過往我們要確保非同步函式完成後才執行某個函式,我們會用到回傳函式的方法來完成,可是如果在該回傳函式裏再接一個回傳函式,形成多層的巢狀結構,就會造成回傳地獄(callback hell)的慘況。但用Promise去寫,就會變得較易閱讀。
繼Promise出現的是async/await,也是目前最新的寫法,它是把Promise寫得更簡潔的語法糖,背後操作原理與Promise是一樣。總括來說,Callback function、Promise、async/await其實都是在解決同一個問題:確保非同步函式完成後才執行某個函式。
Promise基本語法Promise用處:
我們可以透過Promise物件,得出一個非同步函式在完成後,最後得出的成功或失敗結果值。
Promise是一個建構函式,我們要用new來產生一個實體的Promise物件:
//以下只用作解說Promise基本語法,很少會這樣用Promise
const p = new Promise(function(resolve,reject){ //傳入函式,帶有2個参數
//你想執行的非同步工作
setTimeout(function(){
resolve('成功!'); //工作成功,呼叫resolve函式,把結果透過参數傳出去
},1000)
})
在建立Promise物件時,需要傳入一個函式作為参數,該函式裏須帶有兩個参數,分別是resolve和reject函式。
resolve函式:
pending變成fulfilled
reject函式:
pending變成rejected
以上例子是假設Promise所處理的工作結果是成功。但只是寫以上步驟的話,是不會抽取到成功的結果(即是resolve(...)裏的東西)。我們之後還需要在該Promise物件中使用then方法,把函式resolve中的参數(即是成功的結果)抽取出來:
const p = new Promise(function(resolve,reject){
//你想執行的非同步工作
setTimeout(function(){
resolve('成功!'); //工作成功,呼叫resolve函式,把結果透過参數傳出去
},1000)
})
//在Promise物件使用then方法
p.then(function(result){
//result 是任何你由上方 resolve(...) 傳入的東西
console.log(result) //成功!
})
失敗的結果,就可以用catch方法來取得:
const p = new Promise(function(resolve,reject){
setTimeout(function(){
reject('失敗!');
},1000)
})
p.then(function(result){
console.log(result);
})
.catch(function(error){
console.log(error); //失敗!
})
注意:result和error這兩個名字是任你改的。同樣地,在Promise物件裏的resolve和reject這兩個名字都是任你改,只是我們習慣會用resolve和reject。
then也可以抽取失敗結果,但不建議then方法除了可以抽取成功結果,也能抽取失敗結果。我們可以在then方法同時放兩個回傳函式作為参數:
p.then(function(result){
console.log(result);
},function(error){
console.log(error); //失敗!
})
然而,在捕捉錯誤時,用catch寫法比then更好,因為在除錯時,我們可以集中在catch之前的then裏面尋找問題。同時也比較接近以前會用的try/catch寫法。
// 不建議用then寫法
p.then(function(result) {
// success
}, function(error) {
// error
});
// 建議用catch寫法
p.then(function(result) {
// success
})
.catch(function(error) {
// error
});
上面有輕輕提及過Promise可以有3種狀態,包括:pending、fulfilled、rejected。
pending:事件運作中,還未有結果resolved:事件已完成,結果成功,回傳resolve(...)裏的結果rejected:事件已完成,結果失敗,回傳rejected(...)裏的結果Promise要麼回傳resolved,要麼回傳rejected,而且只會回傳一次。
例如我把以上例子改回成功結果,並且在不同位置查看一下Promise的狀態:
const p = new Promise(function(resolve,reject){
setTimeout(function(){
resolve('成功!'); //呼叫resolve函式,會把Promoise狀態變成fulfilled
},1000)
})
p.then(function(result){
console.log(p) //Promise {<fulfilled>: "成功!"}
console.log(result)
})
//p是非同步,所以下面這行會先被執行,這時候p是pending狀態
console.log(p) //Promise {<pending>}
以上例子就是Promise執行完setTimeout函式後得出成功結果後,從pending狀態變成fulfilled狀態的例子。
為什麼最後一行會得出pending,之後上面才得出fulfilled? 因為setTimeout的程式是非同步的,主程式會先跳過它,直接執行最後一行console.log(p),這時候setTimeOut裏的工作並未被執行,所以Promise就是pending狀態。當setTimeout的計時結束,主程式回去執行setTimeOut裏的程式,並且結果是成功,所以Promise就會由pending變成fulfilled。
下圖具體解釋了Promise狀態的變化流程:
圖片來源:JAVASCRIPT.INFO
我們需要完成一個非同步函式後,接著執行下一個非同步函式。這時候我們就可以用鏈接的技巧。以下例子是把兩個數字相加,如果結果是整數就會回傳resolve,負數就會回傳reject。例子参考自這裏。
function add(a,b){
return new Promise( (resolve,reject) => {
window.setTimeout( () => {
let sum = a + b;
sum > 0 ? resolve(`${sum}, 成功`) : reject(`${sum}, 失敗`);
},1000);
})
}
add(10,10)
.then( success => {
console.log(success); //20, 成功
return add(5,10);
})
.then( success => {
console.log(success); //15, 成功
return add(3,-5);
})
.then( success => {
console.log(success);
return add(0,2) //沒有被執行
})
.catch( error => {
console.log(error); //-2, 失敗
})
當其中一個結果(add(3,-5))是失敗時,就會跳過下一個then方法,直接跳到catch方法。所以在以上例子中,add(0,2)是不會被執行。所以要注意,這裏全部4個Promise物件(add(10,10)、add(5,10)、add(3,-5)、add(0,2)),只要其中一個的結果是錯誤,就會跳到catch方法。
在then或catch這些抽取成功或失敗結果值的方法裏,我們不止可以再回傳Promise物件,也可回傳其他表達式。以下範例中,執行完check(2)後,如果check(2)的結果成功,我就回傳另一個函式multiply:
function check(num1){
return new Promise( (resolve,reject) => {
window.setTimeout( () => {
num1 > 0 ? resolve(num1) : reject(`${num1},失敗`)
},1000);
})
}
function multiply(num2){
console.log(`${num2} * 10 = ${num2*10}`) //2 * 10 = 20
}
check(1)
.then( success => {
console.log(success); //1
return check(2)
})
.then( success => {
//回傳其他表達式
return multiply(success);
})
.catch( error => {
console.log(error);
})
Promise常用方法除了then、catch,還有:
Promise.all:Promise函式,並完成所有Promise函式,最後把所有結果集合在一個陣列裏,並且回傳Promise.race:Promise函式,回傳最快完成的那個Promise物件Promise.reject、Promise.resolve:Promise物件的狀態是reject或者resolve
Promise.all以陣列形式傳入多個Promise函式,一定會等到完成所有Promise函式,才會回傳結果,而結果就是一個放有所有Promise物的陣列。例如以下例子,我需要p1和p2都一併完成,才會執行下一個動作:
function add(num1,num2,delayTime){
return new Promise( (resolve,reject) => {
window.setTimeout( () => {
resolve(num1 + num2);
},delayTime)
})
}
const p1 = add(10,20,1000);
const p2 = add(60,70,3000);
Promise.all([p1,p2]).then( success => {
let [result1,result2] = success
console.log(result1,result2) //30,130
})
Promise.race以陣列形式傳入多個Promise函式,回傳最快完成的那個Promise物件。
Promise.race([p1,p2]).then( success => {
console.log(success) //30
})
Promise.reject、Promise.resolve直接定義Promise物件的狀態是reject或者resolve。範例如下:
const p1 = Promise.resolve('Hello World');
const p2 = Promise.reject('Error!');
Promise.all([p1,p2])
.then( success => {
console.log(success);
})
.catch( error => {
console.log(error); //Error!
})
Promise改寫XMLHttpRequest來處理AJAX重溫我們如何用XMLHttpRequest來抓取遠端伺服器的資料:
const xhr = new XMLHttpRequest();
xhr.open('get','https://randomuser.me/api/',true);
xhr.send(null);
//當遠端資料已傳回來,就執行以下函式
xhr.onload = () => {
if(xhr.status === 200){
console.log(xhr.responseText);
}else{
console.log('請求失敗!')
}
}
以上範例以XMLHttpRequest建構函式來產生XMLHttpRequest實體物件,並向遠端發出資料請求。
如果用Promise重寫的話,做法就是把XMLHttpRequest的部分放在Promise裏處理:
function getJSON(url){
return new Promise( (resolve,reject) => {
const xhr = new XMLHttpRequest();
xhr.open('get',url);
xhr.send(null);
xhr.onload = () => {
if(xhr.status === 200){
resolve(JSON.parse(xhr.responseText));
}else{
reject(new Error(xhr.statusText));
}
}
});
};
//把url當作参數
getJSON('https://randomuser.me/api/')
.then(JSONdata => console.log(JSONdata))
.catch(error => console.log(error))
這個做法提高了程式碼的可讀性。如果我們之後又要從不同遠端伺服器抓取資料時,就可以重用getJSON的函式,直接把url當作参數傳進函式裏就行。
你懂 JavaScript 嗎?#24 Promise
Promise 对象
JavaScript Promise 全介紹
有筆誤, 在 「Promise物件的狀態 」 的小節裡:
「resolved:事件已完成,結果成功 略... 」, 應該不是 resolved 而是fulfilled